import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.BitSet;


public class PeerOutputStream {

    private BufferedOutputStream out;
    private GlobalTorrentInfo info;
    public static final boolean DEBUGGING = false;

    public PeerOutputStream (OutputStream out, GlobalTorrentInfo info) {
        if (DEBUGGING)
            this.out = new DebugOutputStream (out);
        else
            this.out = new BufferedOutputStream (out);
        this.info = info;
    }

    private synchronized void sendMsg (byte[] msg) throws IOException {
        out.write(msg);
        out.flush();
    }

    private static byte[] createBytes (int msgLength) {
        return ToolKit.intToBigEndianBytes(msgLength,
                new byte[4 + msgLength], 0);
    }

    public void keepAlive () throws IOException {
        sendMsg(createBytes(0));
    }

    public void handshake (GlobalTorrentInfo info) throws IOException {
        byte[] header = "BitTorrent protocol".getBytes("US-ASCII");
        out.write(19);
        out.write(header);
        out.write(new byte[8]);
        out.write(info.torrentFile.info_hash_as_binary);
        out.write(info.tracker.getPeerIdBytes());
        out.flush();
    }

    public void choke () throws IOException {
        byte[] msg = createBytes(1);
        msg[4] = 0;
        sendMsg(msg);
    }

    public void unchoke () throws IOException {
        byte[] msg = createBytes(1);
        msg[4] = 1;
        sendMsg(msg);
    }

    public void interested () throws IOException {
        byte[] msg = createBytes(1);
        msg[4] = 2;
        sendMsg(msg);
    }

    public void notInterested () throws IOException {
        byte[] msg = createBytes(1);
        msg[4] = 3;
        sendMsg(msg);
    }

    public void have (int pieceIndex) throws IOException {
        byte[] msg = createBytes(5);
        msg[4] = 4;
        msg = ToolKit.intToBigEndianBytes(pieceIndex, msg, 5);
        sendMsg(msg);
    }

    public void bitField (BitSet bits) throws IOException {
        byte[] b = ToolKit.bitSetToBytes(bits);
        int bitSize = (int) Math.ceil((double)info.torrentFile.piece_hash_values_as_binary.size() / 8);
        byte[] msg = ToolKit.intToBigEndianBytes(bitSize+1, new byte[5], 0);
        msg[4] = 5;
        synchronized (this) {
            out.write(msg);
            out.write(b, 0, bitSize);
            out.flush();
        }
    }

    public void request (int index, int begin, int length) throws IOException {
        if (length > 16384)
            throw new IllegalArgumentException ("Cannot request msgs > 16kB");
        byte[] msg = createBytes(13);
        msg[4] = 6;
        msg = ToolKit.intToBigEndianBytes(index, msg, 5);
        msg = ToolKit.intToBigEndianBytes(begin, msg, 9);
        msg = ToolKit.intToBigEndianBytes(length, msg, 13);
        if (GlobalTorrentInfo.debugFile)
            System.out.println("requestpiece: "+index+":"+begin+":"+length);
        sendMsg(msg);
    }

    public void piece (int index, int begin, byte[] block) throws IOException {
        byte[] msg = createBytes(9);
        msg[4] = 7;
        msg = ToolKit.intToBigEndianBytes(9+block.length, msg, 0);
        msg = ToolKit.intToBigEndianBytes(index, msg, 5);
        msg = ToolKit.intToBigEndianBytes(begin, msg, 9);
        synchronized (this) {
            out.write(msg);
            out.write(block);
            out.flush();
        }
        info.tracker.increaseUploadedBy(block.length);
    }

    public void cancel (int index, int begin, int length) throws IOException {
        byte[] msg = createBytes(13);
        msg[4] = 8;
        msg = ToolKit.intToBigEndianBytes(index, msg, 5);
        msg = ToolKit.intToBigEndianBytes(begin, msg, 9);
        msg = ToolKit.intToBigEndianBytes(length, msg, 13);
        sendMsg(msg);
    }

    public void close () throws IOException {
        out.close();
    }

    private static  class DebugOutputStream extends BufferedOutputStream {

        public DebugOutputStream(OutputStream arg0) {
            super(arg0);
        }

        public void write (byte[] b, int off, int len) throws IOException {
            super.write(b, off, len);
            System.out.print(len + ": ");
            for (int k=0; k<len; k++) {
                System.out.print((b[k] & 0xff) + " ");
            }
            System.out.println();
        }

        public void flush () throws IOException {
            super.flush();
            System.out.println();
        }
    }

//  public static void main (String[] args) throws UnknownHostException, IOException, InterruptedException {
//  Socket s = new Socket ((String) null, TrackerInterface.clientPort);
//  PeerOutputStream out = new PeerOutputStream (s.getOutputStream());
//  out.keepAlive();
//  Thread.sleep(5000);
//  out.choke();
//  Thread.sleep(10000);
//  out.close();
//  }
}